package es

import (
	"bytes"
	"errors"
	"fmt"
	"math/rand"
	"net/http"
	"strings"
	"time"
)

func defaultIdGenerator(d interface{}) string {
	t := time.Now().UnixNano()
	r := rand.Int63()
	return fmt.Sprintf("%v-%v", t, r)
}

type esConn struct {
	host string
	cli  *http.Client
}

// Open a new connection to the given server
func NewConn(url string) Conn {
	c := new(esConn)
	c.host = url
	if !strings.HasSuffix(c.host, "/") {
		c.host += "/"
	}
	c.cli = &http.Client{Transport: http.DefaultTransport}
	return c
}

// Open the index for the given name and type.
// The returned index uses the defaultIdGenerator
func (c *esConn) Open(index, tp string) Index {
	i := new(esIndex)
	i.c = c
	i.idx = index
	i.tp = tp
	i.idGen = defaultIdGenerator
	return i
}

// Hold the information to access the ElasticSearch Search Service
type esIndex struct {
	c     *esConn
	idx   string
	tp    string
	idGen IdGenerator
}

// Change the default id generator
// Changing that don't reindex any existing documents
// If the id crashs with another document it will be overwritten
func (i *esIndex) IdGen(idgen IdGenerator) {
	i.idGen = idgen
}

// Store the given data (d) into the pointed index under the given id.
// The data variable right now must be a string but in the future
// this should change to allow any data that can be converted to a JSON string
func (i *esIndex) Write(id string, d interface{}) error {
	if len(id) == 0 {
		id = i.idGen(d)
	}

	body, ok := d.(string)
	if !ok {
		return errors.New("The current implementation only works with string. This will change some day")
	}

	req, err := http.NewRequest("PUT", fmt.Sprintf("%v%v/%v/%v?refresh=true", i.c.host, i.idx, i.tp, id), bytes.NewBufferString(body))

	if err != nil {
		return err
	}

	res, err := i.c.cli.Transport.RoundTrip(req)
	if err != nil {
		return err
	}

	// The server must send an 20x response
	if res.StatusCode < 200 || res.StatusCode >= 300 {
		return errors.New(fmt.Sprintf("Invalid status! Expected %v got %v", "20x", res.StatusCode))
	}
	return nil
}

// Do a Term based search
// The user can pass a list of fields by separating them with comma ",". Value will be checked against all the fields using a boolean query.
// 
// If at least one match is true the document is retreived. This query avoid returning the complete document in order to save network. This
// could change if we find some usage to that.
//
// The function return an channel on which the matching "_id"s will be streamed
func (i *esIndex) Term(fld string, value interface{}) (<-chan string, error) {
	flds := strings.Split(fld, ",")
	m := make(Map)
	clauses := make([]Map, 0)
	for _, v := range flds {
		term := make(Map)
		term.Map("field").Set(v, value)
		clauses = append(clauses, term)
	}

	m, err := i.execBoolShouldQuery(clauses, 1)

	if err != nil {
		return nil, err
	}

	hits := m.Map("hits")
	total := hits["total"].(float64)

	if total == 0 {
		return nil, errors.New("No result found")
	}

	ch := make(chan string, 1)
	i.streamIdResult(hits, ch)

	return ch, nil
}

// Perform a Range based query on one filed
// Unlike the Term query the user cannot pass a list of fields since the lower/upper value usually will only make sense for a paticular field.
//
// Like the Term query the returned result is a channel with all the "_id"s on it
func (i *esIndex) Range(fld string, lower, upper interface{}) (<-chan string, error) {
	m := make(Map)
	clauses := make([]Map, 1)
	clauses[0] = make(Map)
	clauses[0].Map("range").Map(fld).Set("from", lower).Set("to", upper)

	m, err := i.execBoolShouldQuery(clauses, 1)

	if err != nil {
		return nil, err
	}

	hits := m.Map("hits")
	total := hits["total"].(float64)

	if total == 0 {
		return nil, errors.New("No result found")
	}

	ch := make(chan string, 1)
	i.streamIdResult(hits, ch)

	return ch, nil
}

// Execute a POST query request into the server using the bool/should query base
// 
// This is similar to a OR query where every element on the clauses array are evaluated and 
// if at least minimumShould are true the document is considered to match the query.
func (i *esIndex) execBoolShouldQuery(clauses []Map, minimumShould int) (Map, error) {
	m := make(Map)
	m.Map("query").Map("bool").Set("should", clauses).Set("minimum_number_should_match", minimumShould)

	req, err := http.NewRequest("POST", fmt.Sprintf("%v%v/%v/_search", i.c.host, i.idx, i.tp), bytes.NewBufferString(m.String()))

	if err != nil {
		return nil, err
	}

	res, err := i.c.cli.Transport.RoundTrip(req)
	if err != nil {
		return nil, err
	}

	if res.StatusCode < 200 || res.StatusCode >= 300 {
		return nil, errors.New(fmt.Sprintf("Invalid status! Expected %v got %v", "20x", res.StatusCode))
	}

	m, err = FromJson(res.Body)

	if err != nil {
		return nil, err
	}

	return m, nil
}

// Read a ElasticSearch response and streams all the "_id"s.
// This function don't read the source field.
func (i *esIndex) streamIdResult(m Map, output chan string) {
	go func() {
		array := m["hits"].([]interface{})
		for _, v := range array {
			m = CastMap(v)
			output <- m["_id"].(string)
		}
		close(output)
	}()
}
